package com.liuguang.gateway.config.security.impl;

import cn.hutool.core.codec.Base64;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.asymmetric.KeyType;
import cn.hutool.crypto.asymmetric.RSA;
import com.alibaba.fastjson.JSONObject;
import com.liuguang.gateway.config.security.dao.MenuDOMapper;
import com.liuguang.gateway.config.security.dao.UserDOMapper;
import com.liuguang.gateway.config.security.dao.keysDOMapper;
import com.liuguang.api.rabbitmq.RabbitMqService;
import com.liuguang.api.redis.RedisService;
import com.liuguang.api.user.UserService;
import com.liuguang.common.constans.CommonConstans;
import com.liuguang.api.gateway.security.dto.UserInfoDTO;
import com.liuguang.common.enums.CommonResultEnum;
import com.liuguang.common.enums.RedisPreFixEnum;
import com.liuguang.common.pojo.CommonResult;
import com.liuguang.common.pojo.MqData;
import com.liuguang.api.gateway.security.pojo.KeysDOWithBLOBs;
import com.liuguang.api.gateway.security.pojo.MenuDO;
import com.liuguang.api.gateway.security.pojo.RoleDO;
import com.liuguang.api.gateway.security.pojo.UserDO;
import com.liuguang.common.utils.*;
import com.liuguang.gateway.config.security.others.CurrentUserHolder;
import com.liuguang.common.utils.JwtUtil;
import com.liuguang.api.gateway.security.pojo.bo.AuthUser;
import org.apache.commons.lang3.StringUtils;
import org.apache.dubbo.config.annotation.DubboReference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;

import javax.servlet.http.HttpServletRequest;
import javax.validation.constraints.NotNull;
import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.util.*;
import java.util.stream.Collectors;


/**
 * @ClassName: UserserviceImpl
 * @Description： 用户登录服务  使用分布式要集群部署，当前端经过nginx 分发到不同的网关后，进行验证登录、请求发送，如获取公钥的操作，
 * 当插入新的数据，数据库也是集群的，那么如何保证数据库的事务？分布式事务、
 * 分布式锁在哪？都去抢一定数量的货物，那么货物的数量保存在哪？redis缓存、数据库
 * todo： 前端获取到公钥的模和指数，怎么进行加密？
 * @Author: wanghr
 * Date: 5:50 PM 2020/9/11
 * Version: 1.0
 */
@Service
public class UserserviceImpl implements UserService {
    private static final Logger logger = LoggerFactory.getLogger(UserserviceImpl.class);

    @DubboReference(timeout = 40000)
    private RedisService redisService;

//    @DubboReference(timeout = 40000)
    private RabbitMqService rabbitMqService;

    @Autowired
    private keysDOMapper keysDoMapper;

    @Autowired
    private UserDOMapper userDoMapper;

    @Autowired
    private MenuDOMapper menuDOMapper;

    @Value("${rsa.privatekeybase64}")
    private String privateKey;
    @Value("${rsa.publickeybase64}")
    private String publickey;

//    @Autowired
//    private AuthenticationManagerBuilder authenticationManagerBuilder;

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private UserDetailsServiceImpl userservice;

    @Override
    public void test() {
        System.out.println("-----");
//        List<AuthUser> authUserList = userDoMapper.selectByUsername("xiaowang");
//        System.out.println("authUserList = " + authUserList);
//        ===================

        ArrayList<RoleDO> list = new ArrayList<>();
        RoleDO roleDO = new RoleDO();
        roleDO.setId(1);
        list.add(roleDO);
        RoleDO roleDO1 = new RoleDO();
        roleDO1.setId(2);
        list.add(roleDO1);
        List<MenuDO> menuDOList = menuDOMapper.selectByRoleList(list);

        System.out.println(menuDOList.size());
        System.out.println("menuDOList = " + menuDOList);
    }

    @Override
    public CommonResult getPublicKey() {
        //方法一：通过后端生成 公钥私钥，并保存到数据库和redis
//        return getCommonResult();

        //方法二：将公钥私钥以文件形式保存
        //文件保存的是 hutoolUtil 工具类 getPublicKeyBase64() 生成的公钥
//        return getPublicKeyByFile();
        //方法三：直接就保存到配置文件中
        return CommonResult.success(CommonResultEnum.SUCCESS.getMsg(), publickey);
    }

    private CommonResult getPublicKeyByFile() {
        try {
            String publickey = ResourceUtil.getKeyInfo(CommonConstans.PUBLIC_KEY_RESOURCE_URL);
            logger.info("获取公钥成功：{}", publickey);
            return CommonResult.success(CommonResultEnum.SUCCESS.getMsg(), publickey);
        } catch (IOException e) {
            logger.info("获取公钥失败");
            return CommonResult.fail();
        }
    }

    private CommonResult getCommonResult() {
        HashMap<String, String> map = new HashMap<>();
        //先在redis中获取
        // 获取不到去数据库中获取
        CommonResult result = redisService.get(CommonConstans.REDIS_SAVE_KEYS);
        if (CommonResultEnum.SUCCESS.getCode().equals(result.getCode()) && (!Objects.isNull(result.getData()))) {
            String data = (String) result.getData();
            JSONObject redisKeyInfo = JSONObject.parseObject(data);
            String publicExponent = (String) redisKeyInfo.get("publicExponent");
            String modulus = (String) redisKeyInfo.get("modulus");
            map.put("publicExponent", publicExponent);
            map.put("modulus", modulus);
            return CommonResult.success(CommonResultEnum.SUCCESS.getMsg(), map);
        } else {
            List<KeysDOWithBLOBs> list = keysDoMapper.selectByExampleWithBLOBs(null);
            if (!CollectionUtils.isEmpty(list)) {
                KeysDOWithBLOBs privateKeyDo = list.get(0);
                map.put("publicExponent", privateKeyDo.getPublicExponent());
                map.put("modulus", privateKeyDo.getModulus());
                return CommonResult.success(CommonResultEnum.SUCCESS.getMsg(), map);
            } else {
                return getAndSaveKey();
            }
        }


    }

    private CommonResult getAndSaveKey() {
        HashMap<String, Object> keys = null;
        try {
            keys = EncryForRSAUtil.getKeys();
        } catch (NoSuchAlgorithmException e) {
            logger.info("获取公钥失败");
            return CommonResult.fail("获取公钥失败");
        }
        RSAPublicKey publicKey = (RSAPublicKey) keys.get(CommonConstans.PUBLIC_KEY);
        String publicExponent = publicKey.getPublicExponent().toString();
        String modulus = publicKey.getModulus().toString();

        RSAPrivateKey privateKey = (RSAPrivateKey) keys.get(CommonConstans.PRIVATE_KEY);
        String privateExponent = privateKey.getPrivateExponent().toString();
        String modulus1 = privateKey.getModulus().toString();
        //保存到数据库
        save2database(privateExponent, modulus1, publicExponent);
        //保存到redis中
        save2redis(publicExponent, modulus, privateExponent);

        //公钥的模和指数 返回前端
        HashMap<String, String> map = new HashMap<>();
        map.put("publicExponent", publicExponent);
        map.put("modulus", modulus);
        return CommonResult.success(CommonResultEnum.SUCCESS.getMsg(), map);
    }

    private void save2redis(String publicExponent, String modulus, String privateExponent) {
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("modulus", modulus);
        jsonObject.put("privateExponent", privateExponent);
        jsonObject.put("publicExponent", publicExponent);
        String string = jsonObject.toJSONString();
        redisService.set(CommonConstans.REDIS_SAVE_KEYS, string);
        logger.info("私钥的模数和指数保存到redis成功");
    }

    private void save2database(String privateExponent, String modulus1, String publicExponent) {
        //私钥保存到数据库和redis
        // todo: 数据库不能只保存私钥信息，也要保存公钥指数， 模数是相同的
        KeysDOWithBLOBs privateKeyDo = new KeysDOWithBLOBs();
        privateKeyDo.setModulus(modulus1);
        privateKeyDo.setPrivateExponent(privateExponent);
        privateKeyDo.setPublicExponent(publicExponent);
        privateKeyDo.setCreateTime(new Date());
        keysDoMapper.insert(privateKeyDo);
        logger.info("私钥的模数和指数保存到数据库成功");
    }

    @Override
    public CommonResult getVerificationCode() {
        //当前时间作为 key
        String id = UUID.randomUUID().toString();
        //默认获取 四位验证码
        String code = RandomUtil.randomString(4);
        //保存到redis中
        try {
            logger.info("验证码：{}", code);
            String key = RedisPreFixEnum.VERIFY_CODE_USERNAME_PASSWORD_TYPE.getValue() + id;
            redisService.setWithTime(key, code, 3L);
            HashMap<String, String> map = new HashMap<>();
            map.put("codeKey", id);
            map.put("code", code);
            return CommonResult.success(CommonResultEnum.SUCCESS.getMsg(), map);
        } catch (Exception e) {
            logger.info("redis 存储验证码失败");
//            e.printStackTrace();
            return CommonResult.fail(CommonResultEnum.FAIL.getMsg(), e);
        }
    }

    @Override
    public CommonResult<?> userEnroll(UserInfoDTO userInfoDTO, HttpServletRequest request) {
        logger.info("获取到参数：{}", userInfoDTO);
        //判断验证码是否正确，删除redis中信息
        CommonResult result = verifyCode(userInfoDTO.getCodeKey(), userInfoDTO.getVerificationCode()
                , RedisPreFixEnum.VERIFY_CODE_USERNAME_PASSWORD_TYPE.getValue());
        if (CommonResultEnum.FAIL.getCode().equals(result.getCode())) {
            return result;
        }
        //模拟前端对密码,进行Rsa加密.
        // todo:有前端后，注掉。
        mockEncode(userInfoDTO);

        //保存用户信息到数据库
        UserDO userDo = new UserDO();
        BeanUtils.copyProperties(userInfoDTO, userDo);
        //rsa 私钥解密，得到用户输入 Password，使用 bcrypt 加密
        String password = getBcryptPassword(userInfoDTO);
        String bcryptPassword = new BCryptPasswordEncoder().encode(password);
        userDo.setPassword(bcryptPassword);
        userDo.setFrequentlyUsedIp(WebUtil.getIp(request));
        userDo.setIsDelated(CommonConstans.DELATE_DEFULT);
        userDo.setCreateTime(new Date());
        logger.info("存入数据库用户信息：{}", userDo);
        userDoMapper.insert(userDo);

        return CommonResult.success();
    }

    private CommonResult<?> verifyCode(String key, String code, String type) {
        key = type + key;
        CommonResult<String> result = redisService.get(key);
        String resultData = result.getData();
        if (Objects.isNull(resultData)) {
            return CommonResult.fail(CommonConstans.REDIS_ACQUARE_CODE_FAIL);
        }
        if (!StringUtils.equals(resultData, code)) {
            return CommonResult.fail(CommonConstans.VERIFY_CODE_FAIL);
        }
        CommonResult redisResult = redisService.deleteBykey(key);
        if (!CommonResultEnum.SUCCESS.getCode().equals(redisResult.getCode())) {
            return CommonResult.fail(CommonConstans.REDIS_VERIFY_CODE_DELATE_FAIL);
        }
        return CommonResult.success(CommonConstans.VERIFY_CODE_SUCCESSL);
    }

    @Override
    public CommonResult login(UserInfoDTO userInfoDTO, HttpServletRequest request) {
        //模拟前端对密码进行Rsa加密,再base64 加密
        // todo:有前端后，注掉。
        mockEncode(userInfoDTO);

        //获取验证码，验证码信息
//        @NotNull String codeKey = userInfoDTO.getCodeKey();
//        @NotNull String verificationCode = userInfoDTO.getVerificationCode();
        @NotNull String username = userInfoDTO.getUsername();
//        CommonResult result = verifyCode(codeKey, verificationCode, RedisPreFixEnum.VERIFY_CODE_USERNAME_PASSWORD_TYPE.getValue());
//        if (CommonResultEnum.FAIL.getCode().equals(result.getCode())){ return result;}
        logger.info("验证码验证成功");

        //获取前端传来得密码，先base64 解密，在 rsa 解密，使用 bcrypt 加密
        String password = getBcryptPassword(userInfoDTO);

        //委托security框架去验证
        // 注入 AuthenticationManager 进行验证，验证通过后把用户信息保存到SecurityContextHolder中
        // 那么分布式中，其他得服务是获取不到当前在线用户的信息的，
        // 把这部分代码放到 gateway模块，在其他模块需要当前得用户信息的时候，可以通过参数传过去
        // 那么为什么不直接放到 token 中，不在SecurityContextHolder 获取当前用户信息，
        // 但是其他的接口是需要鉴权的，所以这接口应该在gateway中，
        // 其实也可以将用户的认证信息返回给gateway，没必要，在jwt 中获取就行了
        //感觉是用分布式，反而增加了难度，两个项目中的security 配置不同，但是不知道是否影响验证？
        //最终还是将所有的东西都弄到 网关模块，不然委托sucurity 进行验证的时候 bcrypt 解密对象不能使用一个不能完成密码的比对

        Authentication authenticate = null;
        try {
            //通过 authenticationManagerBuilder 获取也性
//            AuthenticationManager authenticationManager = authenticationManagerBuilder.getObject();
            authenticate = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username,
                    password));
        } catch (Exception e) {
            logger.info("验证出异常");
            e.printStackTrace();
            return CommonResult.fail(CommonConstans.SECURITY_FAIL);
        }

        //创建 JWT
        logger.info("验证成功，开始创建token,authenticate:{}", authenticate);
        String token = getToken(authenticate);
        logger.info("用户：{},token：{}", username, token);

        //验证是否是常用ip，不是常用ip，使用rabbitmq，向用户发送邮件通知
        verifyIPAndSaveUser(request);
        //返回用户的菜单
        return CommonResult.success(CommonResultEnum.SUCCESS.getMsg(), token);
    }

    private String getToken(Authentication authenticate) {
        //redis 保存 token 加密的key，供网关解密使用
        String key = RedisPreFixEnum.JWT_CLAIM_KEY.getValue() + "";
        CommonResult<String> result = redisService.get(key);
        if (CommonResultEnum.FAIL.getCode().equals(result.getCode())) {
            String jwtClaimKey = JwtUtil.getJwtClaimKey();
            logger.info("redis 保存 jwtClaimKey：{}",jwtClaimKey);
            redisService.set(key,jwtClaimKey);
        }

        //创建 jwt 并传入参数：在claim中的加密数据
        String username = ((User) authenticate.getPrincipal()).getUsername();
        Collection<? extends GrantedAuthority> authorities = authenticate.getAuthorities();
        String auth = authorities.stream().map(GrantedAuthority::getAuthority)
                .collect(Collectors.joining(","));
        HashMap<String, Object> map = new HashMap<>();
        map.put("username",username);
        map.put("auth", auth);
        return JwtUtil.createToken(map);
    }

    private void verifyIPAndSaveUser(HttpServletRequest request) {
        AuthUser currentUser = CurrentUserHolder.getCurrentUser();
        String username = currentUser.getUsername();
        String frequentlyUsedIp = currentUser.getFrequentlyUsedIp();
        String ip = WebUtil.getIp(request);
        if (!StringUtils.equals(ip, frequentlyUsedIp)) {
            //消息队列 发送邮件
            logger.info("当前IP：{}与默认ip：{}不一致，给用户：{},发送邮件", ip, frequentlyUsedIp, username);
//
            MqData mqData = new MqData();
            mqData.setMsg("发送邮件：为当前用户发送邮件");
            HashMap<String, Object> data = new HashMap<>();
            data.put("ip", ip);
            data.put("username", username);
            data.put("userEmail", currentUser.getEmail());
            mqData.setData(data);
//
//            rabbitTemplate.convertAndSend(CommonConstans.DIRECT_EXCHANGE_NAME,CommonConstans.EMAIL_ROUTING_KEY,mqData,corrData);
        }

        //saveUser
        //redis中缓存用户信息一天
        String key = RedisPreFixEnum.SAVE_USER_INFO.getValue() + username;
        String userInfo = JSONObject.toJSONString(currentUser);
        redisService.setWithTime(key,userInfo,24*60L);
    }

    @Override
    public CommonResult getVerificationCodeByPhone(String phoneNumber) {
        Assert.notNull(phoneNumber, CommonConstans.PHONE_MUST_NOT_NULL);
        //判断是否已经给该手机号发送过短信，防止用户多次请求，也可以前端在设置点击一次后按钮置灰
        String key = RedisPreFixEnum.VERIFY_CODE_PHONE_TYPE.getValue() + phoneNumber;
        CommonResult<String> redisResult = redisService.get(key);
        if (CommonResultEnum.FAIL.getCode().equals(redisResult.getCode())) {
            return CommonResult.fail();
        } else {
            if (StringUtils.isNotEmpty(redisResult.getData())) {
                return CommonResult.fail(CommonConstans.CODE_SENDED);
            }
        }
        //手机接收短信，验证码
        String code = RandomUtil.randomNumbers(4);
        //保存验证码到 redis 设置保存时间是5分钟
        logger.info("向{} 发送验证码：{}", phoneNumber, code);
        redisService.setWithTime(key, code, 5000L);
        return SmsUtil.sendSms(phoneNumber, code);
    }

    @Override
    public CommonResult loginByPhone(UserInfoDTO userInfoDTO) {
        //手机号形式登录一定能成功

        @NotNull String verificationCode = userInfoDTO.getVerificationCode();
        String phoneNumber = userInfoDTO.getPhoneNumber();
        //验证手机号和验证码两项必须非空
        Assert.notNull(phoneNumber, CommonConstans.PARAM_NULL);
        Assert.notNull(verificationCode, CommonConstans.PARAM_NULL);
        //验证码 验证
        String key = RedisPreFixEnum.VERIFY_CODE_PHONE_TYPE.getValue() + phoneNumber;
        CommonResult<String> redisResult = redisService.get(key);
        if (CommonResultEnum.FAIL.getCode().equals(redisResult.getCode())) {
            return CommonResult.fail();
        } else {
            if (StringUtils.isEmpty(redisResult.getData())) {
                return CommonResult.fail(CommonConstans.CODE_TIME_OUT);
            }
        }
        if (!StringUtils.equals(redisResult.getData(), verificationCode)) {
            return CommonResult.fail(CommonConstans.VERIFY_CODE_FAIL);
        }
        CommonResult result = redisService.deleteBykey(key);
        logger.info(CommonConstans.VERIFY_CODE_SUCCESSL);

        //判断手机号是否有账号
        UserDO userDo = new UserDO();
        userDo.setPhoneNumber(phoneNumber);
        List<UserDO> userDos = userDoMapper.selectByCondition(userDo);
        if (userDos.size() > 1) {
            logger.info(CommonConstans.HASCOUNT_MORETAN_ONE);
            return CommonResult.fail(CommonConstans.HASCOUNT_MORETAN_ONE);
        } else if (userDos.size() == 1) {
            logger.info(CommonConstans.HASCOUNT);
            return CommonResult.success(CommonConstans.HASCOUNT);
        }
        //没有账号新增账号
        UserDO newUser = new UserDO();
        newUser.setUsername(RandomUtil.randomString(5));
        newUser.setPhoneNumber(phoneNumber);
        newUser.setIsDelated(CommonConstans.DELATE_DEFULT);
        newUser.setCreateTime(new Date());
        newUser.setUpdateTime(new Date());
        //int insert = userDoMapper.insert(newUser);


        //todo:1、还用根据用户信息创建token？ 目测不用
        return CommonResult.success();
    }


    private String getBcryptPassword(UserInfoDTO userInfoDTO) {
        RSA rsa = SecureUtil.rsa(privateKey, publickey);
        byte[] decode = Base64.decode(userInfoDTO.getPassword());
        byte[] decrypt = rsa.decrypt(decode, KeyType.PrivateKey);
        return new String(decrypt);
    }

    private void mockEncode(UserInfoDTO userInfoDTO) {
//        logger.info("{}",privateKey);
//        logger.info("{}",publickey);
        RSA rsa0 = SecureUtil.rsa(privateKey, publickey);
        @NotNull String infoDTOPassword = userInfoDTO.getPassword();
        logger.info("前端传来密码：{}", infoDTOPassword);
        byte[] encrypt = rsa0.encrypt(userInfoDTO.getPassword(), KeyType.PublicKey);
        String encodePassword = Base64.encode(encrypt);
        logger.info("前端使用公钥加密,在使用base64 加密后后密码：{}", encodePassword);
        userInfoDTO.setPassword(encodePassword);
    }
}
